package za.org.jcicada.katcp;

/**
 * Message.java
 *
 * @author Simon Cross
 *
 * Created on October 03, 2008, 13:45
 */

import za.org.jcicada.katcp.KatcpSyntaxException;

import java.util.Map;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

/**
 * Represents a KAT device control language message.
 */
public class Message {

    /**
     * Message types.
     */
    public enum Type {
        /** A request message. */ REQUEST,
        /** A reply message. */ REPLY,
        /** An inform message. */ INFORM;
    };

    /**
     * Reply codes.
     */
    public enum Code {
        /** Successful response. */ OK,
        /** Request failed. */ FAIL,
        /** Request was not valid. */ INVALID;
    };

    /**
     * Map from message Type to character for encoding the message type.
     */
    public static final Map<Type, String> TYPE_SYMBOLS
        = new EnumMap<Type, String>(Type.class);
    static {
        TYPE_SYMBOLS.put(Type.REQUEST, "?");
        TYPE_SYMBOLS.put(Type.REPLY, "!");
        TYPE_SYMBOLS.put(Type.INFORM, "#");
    }

    /**
     * Map from message type characters to Type values.
     */
    public static final Map<String, Type> TYPE_SYMBOL_LOOKUP
        = new HashMap<String, Type>();
    static {
        for (Map.Entry<Type, String> entry : TYPE_SYMBOLS.entrySet())
            TYPE_SYMBOL_LOOKUP.put(entry.getValue(), entry.getKey());
    }

    /** 
     * Map from escape codes to the values they represent.
     */
    public static final Map<String, String> ESCAPE_LOOKUP
        = new HashMap<String, String>();
    static {
        ESCAPE_LOOKUP.put("\\", "\\");
        ESCAPE_LOOKUP.put("_", " ");
        ESCAPE_LOOKUP.put("0", "\0");
        ESCAPE_LOOKUP.put("n", "\n");
        ESCAPE_LOOKUP.put("r", "\r");
        ESCAPE_LOOKUP.put("e", "\33"); // escape
        ESCAPE_LOOKUP.put("t", "\t");
        ESCAPE_LOOKUP.put("@", "");
    }

    /**
     * Map from characters that need escaping to their escape codes.
     */
    public static final Map<String, String> REVERSE_ESCAPE_LOOKUP
        = new HashMap<String, String>();
    static {
        for (Map.Entry<String, String> entry : ESCAPE_LOOKUP.entrySet())
            REVERSE_ESCAPE_LOOKUP.put(entry.getValue(), entry.getKey());
    }

    /**
     * Regular expression Pattern matching characters that require escaping.
     */
    public static final Pattern ESCAPE_RE = Pattern.compile("[\\\\ \\00\\n\\r\\x1b\\t]");

    /**
     * Regular expression Pattern matching a Message name.
     */
    protected static final Pattern NAME_RE = Pattern.compile("[a-zA-Z][a-zA-Z0-9-]*");

    // Instance variables

    /**
     * The Type of this Message.
     */
    protected Type msgType;

    /**
     * The message name.
     */
    protected String name;

    /**
     * The message arguments.
     */
    protected String[] arguments;

    /**
     * Create new Message object.
     *
     * @param msgType  Message Type.
     * @param name  Message name.
     * @param arguments  Message arguments.
     */
    public Message(Type msgType, String name, String[] arguments) { 
        this.msgType = msgType;
        this.name = name;
        if (arguments == null)
            this.arguments = new String [0];
        else
            this.arguments = (String []) (arguments.clone());

        // check command name

        if (name.length() == 0)
            throw new KatcpSyntaxException("Message missing command name.");

        if (!NAME_RE.matcher(name).matches())
            throw new KatcpSyntaxException("Message name should consist only"
                                + " of alphanumeric characters and dashes and"
                                + " should begin with alphabetic character"
                                + " (got " + name + ").");
    }

    /**
     * Serialize message for sending over the wire.
     *
     * @return  The message encoded as a KATCP message string.
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();
        Matcher matcher;
        String escape;

        if (arguments.length > 0) {
            for (String arg : arguments) {
                buffer.append(" ");
                if (arg.length() == 0) {
                    buffer.append("\\@");
                    continue;
                }
                matcher = ESCAPE_RE.matcher(arg);
                while (matcher.find()) {
                    // double escape the \ because matcher.appendReplacement
                    // also unescapes it.
                    escape = "\\" + REVERSE_ESCAPE_LOOKUP.get(matcher.group());
                    escape = escape.replaceAll("\\\\","\\\\\\\\");
                    matcher.appendReplacement(buffer, escape);
                }
                matcher.appendTail(buffer);
            }
        }
        
        return TYPE_SYMBOLS.get(msgType) + name + buffer.toString();
    }

    /**
     * Return the message name.
     *
     * @return  The message name.
     */
    public String getName() {
        return name;
    }

    /**
     * Return the message Type.
     *
     * @return  The message Type.
     */
    public Type getType() {
        return msgType;
    }

    /**
     * Return the message arguments.
     *
     * @return  The message arguments.
     */
    public String[] getArguments() {
        return (String []) (arguments.clone());
    }

    /**
     * Helper method for creating request messages.
     *
     * @param name  The message name.
     * @param arguments  The message arguments.
     * @return  The new message.
     */
    public static Message request(String name, String... arguments) {
        return new Message(Type.REQUEST, name, arguments);
    }

    /**
     * Helper method for creating reply messages.
     *
     * @param name  The message name.
     * @param arguments  The message arguments.
     * @return  The new message.
     */
    public static Message reply(String name, String... arguments) {
        return new Message(Type.REPLY, name, arguments);
    }

    /**
     * Helper method for creating inform messages.
     *
     * @param name  The message name.
     * @param arguments  The message arguments.
     * @return  The new message.
     */
    public static Message inform(String name, String... arguments) {
        return new Message(Type.INFORM, name, arguments);
    }

}
